How the Commonwealth of Massachusetts Covering A Greater Proportion of Public School Funding Through State Level Revenue Would Help Reduce NIMBYism
Author: Conor Sheehy
Some Implications for Economic Development & Relevant Massachusetts Education Data, Charts, and Tables
The United States and other anglophone countries like the U.K. and Australia, which share similar regulatory regimes, often face significant challenges in building major intrastate and interstate projects. This difficulty arises from the vast number of stakeholders involved and poorly thought-out laws that allow vocal minorities and special interest groups to intervene and halt progress. NIMBYism (Not In My Backyard) is a scourge on economic growth, reducing standards of living for all citizens, particularly low-income individuals. Arbitrary rules and delays prevent even commonsense and high-benefit/cost projects, such as a high-speed rail from Boston to Washington, from being realized.
While projects are challenging enough to complete when managed by federal and state officials, the outsourcing of state legislative power over land use and zoning to municipal authorities presents nearly insurmountable economic development, city growth, and housing cost challenges. Individual property owners have powerful incentives to ensure housing scarcity to prop up housing prices and protect local resources like school funding, driving NIMBYism. This, in turn, leads to project delays and increased costs. When beneficial projects do get off the ground, they often come in far above the initial budget due to lawsuits and lack of cooperation, and they take years or even decades longer to complete compared to countries such as France and Japan.
At all levels of government in the United States, we arbitrarily subsidize homeownership rather than prioritizing housing affordability. As an indirect consequence of producing laws intending to make homeownership more accessible, we've actually failed at that goal while also inadvertently making housing more expensive and scarce for both homebuyers and renters. Homeowners reasonably focus intensely on the downside risks to their home equity, which further fuels NIMBYism. The economics of restricting development are simple: by using legal maneuvering and restrictions to block new residential development, output of residential units collapses, and in strong labor markets with strong university systems like Boston and New York City, reliably increasing demand for shelter drives up prices for the existing housing stock. For existing homeowners and landlords, their assets appreciate. For renters, rents reliably increase year after year, and on the margin, the lowest income renters are forced to make hard choices: move to a city with a worse labor market and cheaper housing, get more roommates, or become the working homeless.
The politics of zoning and permitting is fraught. Many NIMBYs are not unreasonable people. Rather than assuming NIMBYs are naturally opposed to development, it is clear that government policy has distorted individual incentives, making people more likely to block development than they would otherwise.
I aim to tackle just one policy that makes development so difficult in Massachusetts: Local Property Taxes being used to fund public schools. I propose two pathways to address this issue: pushing as much collection of revenue/funding as possible to the state level, and replacing property tax with land value tax as a less distortionary replacement. The first pathway can be implemented through various state-level tax approaches, while the second pathway is more speculative and would require a larger adjustment. My approach can be extended to any other state that provides per pupil spending data by district/town/city.
Currently, most municipal revenue in Massachusetts is collected through property tax, Chapter 70 state foundation budget school aid, and user fees for municipal services like sewer and water. Relative to land value tax, property tax is suboptimal because it decreases marginal investment in property improvements. If a dilapidated building is improved or a parking lot is replaced with a building, the owner faces a higher tax bill in perpetuity. The government ought to encourage these sorts of improvements rather than taxing them. While there are concerns around implementation, a land value tax would be preferable to property tax because land supply is typically highly inelastic/fixed. Land value tax rewards the efficient use of land and prompts owners that are using valuable land inefficiently to develop or sell to someone who will. This would shift individual property owners' incentives toward up-zoning, remove disincentives for making home renovations, and potentially soften NIMBY opposition.
Property tax assessments already require land value assessments as a component, however, the primary objective of current assessments is the accurate valuation of the overall property, rather than the residual value of the land. Barring environmental pollution or large geophysical disparities, the assessed value per acre of directly adjacent equivalent plots ought to be priced similarly. An evaluation of feasibility needs to be done, but given that property tax assessments are already done on a large scale despite being done on illiquid assets, determining residual values of land should not be a dealbreaker if the political will is there. This document does not assess the distributional effects of the land value tax - to do that I need to first collect and aggregate data on land values to establish some reasonable baseline level of accuracy. An attractive political feature of a land value tax is that the legislature is reclaiming some control over land use and zoning from municipalities indirectly and could split the Mass Municipal Association's alignment based on their change in relative tax burdens under the new approach.
The main problem I foresee with this approach is that K-12 education funding varies considerably by town and is primarily funded through property tax and Massachusetts Foundation budget assistance from the state government, so it's not possible to directly phase out property tax at the state level. However, based on my analytics below, I believe that substantial progress can be made to decouple resistance to up-zoning and deregulation based on local pooling of school funding, while preserving the current variation in per-pupil spending.
Using the data aggregations below, my idea is this: phase in a land value tax (with reasonable deadlines for municipal adjustments) that replaces roughly 13,500 to 15,000 USD per student (1st to 5th percentile per pupil spending) and allocate to schools based on headcount statewide. All towns would retain the ability to levy a local property tax or replace their property tax with an additional local land value tax. Municipalities are most likely to respond to the subsidy by reducing local property tax levels to keep per-pupil spending constant. Looking at the median town which spends ~$18,700/pupil, roughly 80% (15k/18.7k) would be state-funded. Towns like Newton that spend closer to 25k would see ~60% replaced. A large proportion of schooling costs related to inflows of children from new local developments would be collectively handled by the entire state, and thus lessen hostility to heterogenous population growth rates by town. Additionally, while an outlier individual town could see a 20-30% year-over-year increase in population with new developments in a much looser regulatory regime, this is highly unlikely at the state level due to its sheer size.
In the short run, low density relative to land value places like Brookline could potentially see increased taxes overall. It is notable however, that many Homeowners and Renters may see decreases in their relative tax burden in municipalities with large underutilized spaces like empty parking lots, golf courses in urban areas, or large differences in lot size by area of town (Brookline for instance has a dense area near the Greenline C branch, while the southern part of the town has more mini-mansions and larger lot sizes).
I've attached charts and tables with 2021 Massachusetts education spending data (some are charter, some are districts, and some are towns/cities so it isn't totally consistent). The table entitled "QuantileData" shows quantile information for Total Expenditures per Pupil per school across the data and high and low estimates for state revenue collections needed to replace the per-pupil expenditures for all students statewide for various levels of per pupil spending (High of 992k students and low of 883k students. 65 to 100k variation based on which source/year is used and which private/parochial school figures are used).
The State already provides about $6.6 Billion of state assistance through Chapter 70 foundation budget requirements, so this policy could be coupled with cuts to other state taxes that are more distortionary, like income. Since Massachusetts is more generous than most other states in education spending for children, as more housing gets built and prices come down, out-of-state resident parents would be incentivized to move into Massachusetts. While this could at a later date present a fiscal burden, this is dependent on the characteristics of the movers; this effect wouldn't play out in a significant way until housing costs have already dipped considerably since Mass already has some of the highest housing costs in the country and it will take quite some time for major policy changes to play out.
For additional context, the Mass State budget is anticipated at ~ 56-58 billion for 2024. Mass GDP is projected at 720 billion annualized for 2023, so bear in mind that these are 2021 Education/2023 GDP figures, so I'm probably off by about 0.1-0.3% for % GDP figures in the QuantileData table. Beyond the pair plot (selected scatterplots) at the bottom of the page are several charts related to school characteristics and their relationship to the ethnic makeup of students, as well as data aggregations across the state relative to the town of Newton, MA, where the Newton Teacher's Association recently held a strike.
'''Data is from the Massachusetts Department of Education here:
https://profiles.doe.mass.edu/statereport/ '''
# Import Relevant Packages
import platform
import numpy as np
import matplotlib as plt
import pandas as pd
import seaborn as sns
import seaborn.objects as so
import re
import warnings
# Package Versions For Current Run
print('python ' + platform.python_version())
print('numpy: ' + np.__version__)
print('matplotlib: ' + plt.__version__)
print('pandas: ' + pd.__version__)
print('seaborn: ' + sns.__version__)
print('regex: ' + re.__version__)
python 3.11.5 numpy: 1.24.3 matplotlib: 3.8.0 pandas: 2.1.1 seaborn: 0.12.2 regex: 2.2.1
'''
import warnings in case they pop up on a future run'''
# Ignore DTypeWarnings and FutureWarnings
warnings.filterwarnings("ignore", "is_categorical_dtype")
warnings.filterwarnings("ignore", "use_inf_as_na")
warnings.simplefilter("ignore", category=FutureWarning)
# Import Spending Figures
MassPerPupilSpend = pd.read_excel('PerPupilExpenditures.xlsx', sheet_name='Per Pupil Expenditures', skiprows=1, nrows=399)
MassPerPupilSpend = MassPerPupilSpend.astype('string')
# Separate to Speed up String Replace Operations and data type changes
District_Name_Code = MassPerPupilSpend[['District Name','District Code','In-District Expenditures']]
MassPerPupilSpend = MassPerPupilSpend.drop(['District Name','District Code'],axis=1)
District_Name_Code['In-District Expenditures'].replace(['\$',' ',','],'',regex=True,inplace=True)
District_Name_Code['District Code'] = District_Name_Code['District Code'].astype('int')
District_Name_Code['In-District Expenditures'] = District_Name_Code['In-District Expenditures'].astype('float')
MassPerPupilSpend = MassPerPupilSpend.replace(['\$',' ',','],'',regex=True)
MassPerPupilSpend = MassPerPupilSpend.astype('float')
# Merge Constituent Dataframes
MassPerPupilSpend = District_Name_Code.merge(MassPerPupilSpend,on='In-District Expenditures')
# Sort Dataframe by Pupils and Expenditures Per Pupil
MassPerPupilSpend = MassPerPupilSpend.sort_values(['Total Pupil FTEs','Total Expenditures per Pupil'], ascending=False)
MassPerPupilSpend.set_index('District Name')
MassPerPupilSpend.info()
<class 'pandas.core.frame.DataFrame'> Index: 399 entries, 398 to 311 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 District Name 399 non-null string 1 District Code 399 non-null int64 2 In-District Expenditures 399 non-null float64 3 Total In-district FTEs 399 non-null float64 4 In-District Expenditures per Pupil 399 non-null float64 5 Total Expenditures 399 non-null float64 6 Total Pupil FTEs 399 non-null float64 7 Total Expenditures per Pupil 399 non-null float64 dtypes: float64(6), int64(1), string(1) memory usage: 28.1 KB
# Import Teacher Salary Dataset
TeacherData = pd.read_excel('TeacherSalaries.xlsx', sheet_name='Teacher Salaries', skiprows=1, nrows=399)
# Convert to Strings for Regex
TeacherData['District Name'] = TeacherData['District Name'].astype('string')
TeacherData['Salary Totals'] = TeacherData['Salary Totals'].astype('string')
TeacherData['Average Salary'] = TeacherData['Average Salary'].astype('string')
TeacherData['FTE Count'] = TeacherData['FTE Count'].astype('string')
# Regex String Replacement/Cleanup, replace does full matching without regex
TeacherData['Salary Totals'].replace(['\$',' ',','],'',regex=True,inplace=True)
TeacherData['Average Salary'].replace(['\$',' ',','],'',regex=True,inplace=True)
TeacherData['FTE Count'].replace(['\$',' ',','],'',regex=True,inplace=True)
# Conversion back to Floats
TeacherData['Salary Totals'] = TeacherData['Salary Totals'].astype('float')
TeacherData['Average Salary'] = TeacherData['Average Salary'].astype('float')
TeacherData['FTE Count'] = TeacherData['FTE Count'].astype('float')
TeacherData
| District Name | District Code | Salary Totals | Average Salary | FTE Count | |
|---|---|---|---|---|---|
| 0 | Abby Kelley Foster Charter Public (District) | 4450000 | 7586419.00 | 66422.00 | 114.20 |
| 1 | Abington | 10000 | 12181431.00 | 92005.00 | 132.40 |
| 2 | Academy Of the Pacific Rim Charter Public (Dis... | 4120000 | 3503450.00 | 67505.00 | 51.90 |
| 3 | Acton-Boxborough | 6000000 | 36991103.00 | 91268.00 | 405.30 |
| 4 | Acushnet | 30000 | 5221793.00 | 76566.00 | 68.20 |
| ... | ... | ... | ... | ... | ... |
| 391 | Woburn | 3470000 | 32234181.00 | 89168.00 | 361.50 |
| 392 | Worcester | 3480000 | 171799156.00 | 93339.00 | 1840.60 |
| 393 | Worthington | 3490000 | 322221.00 | 43543.00 | 7.40 |
| 394 | Wrentham | 3500000 | 6481188.00 | 87348.00 | 74.20 |
| 395 | State Totals | 0 | 6461729083.00 | 86118.00 | 75033.20 |
396 rows × 5 columns
# Drop State Totals for Data Visualizations by City/Town
TownsOnlySpend = MassPerPupilSpend[MassPerPupilSpend['District Name'] != 'State Totals']
TownsOnlySpend
# Remove State Totals for Data Visualizations
TownsOnlyTeachers = TeacherData[TeacherData['District Name'] != 'State Totals']
# Generate Primary Dataset with Teacher + Student Data
AllTowns = TownsOnlySpend.merge(TownsOnlyTeachers,on=['District Name','District Code'])
# Proportion of District Expenditures going to Teachers
AllTowns['TeacherProportion'] = AllTowns['Salary Totals']/AllTowns['Total Expenditures']
# Student Teacher Ratio Calculation
AllTowns['StudentTeacherRatio'] = AllTowns['Total In-district FTEs']/AllTowns['FTE Count']
AllTowns.info()
AllTowns
<class 'pandas.core.frame.DataFrame'> RangeIndex: 395 entries, 0 to 394 Data columns (total 13 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 District Name 395 non-null string 1 District Code 395 non-null int64 2 In-District Expenditures 395 non-null float64 3 Total In-district FTEs 395 non-null float64 4 In-District Expenditures per Pupil 395 non-null float64 5 Total Expenditures 395 non-null float64 6 Total Pupil FTEs 395 non-null float64 7 Total Expenditures per Pupil 395 non-null float64 8 Salary Totals 395 non-null float64 9 Average Salary 395 non-null float64 10 FTE Count 395 non-null float64 11 TeacherProportion 395 non-null float64 12 StudentTeacherRatio 395 non-null float64 dtypes: float64(11), int64(1), string(1) memory usage: 40.2 KB
| District Name | District Code | In-District Expenditures | Total In-district FTEs | In-District Expenditures per Pupil | Total Expenditures | Total Pupil FTEs | Total Expenditures per Pupil | Salary Totals | Average Salary | FTE Count | TeacherProportion | StudentTeacherRatio | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Boston | 350000 | 1348915645.82 | 48747.80 | 27671.31 | 1676837932.25 | 63064.70 | 26589.17 | 481668763.00 | 104813.00 | 4595.50 | 0.29 | 10.61 |
| 1 | Springfield | 2810000 | 461156621.50 | 24078.80 | 19151.98 | 552515849.99 | 29638.00 | 18642.14 | 155184202.00 | 74014.00 | 2096.70 | 0.28 | 11.48 |
| 2 | Worcester | 3480000 | 411191518.17 | 24019.10 | 17119.36 | 466801954.33 | 26958.00 | 17315.90 | 171799156.00 | 93339.00 | 1840.60 | 0.37 | 13.05 |
| 3 | Lynn | 1630000 | 248487773.52 | 15549.50 | 15980.43 | 290636775.52 | 17774.10 | 16351.70 | 94658722.00 | 82162.00 | 1152.10 | 0.33 | 13.50 |
| 4 | Brockton | 440000 | 260837443.86 | 15187.20 | 17174.82 | 290538465.60 | 17022.80 | 17067.61 | 101375149.00 | 102794.00 | 986.20 | 0.35 | 15.40 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 390 | Hancock | 1210000 | 989688.30 | 54.60 | 18126.16 | 1613159.14 | 98.10 | 16444.03 | 500535.00 | 67640.00 | 7.40 | 0.31 | 7.38 |
| 391 | Florida | 980000 | 1761492.07 | 84.60 | 20821.42 | 1935881.75 | 95.90 | 20186.46 | 509459.00 | 47172.00 | 10.80 | 0.26 | 7.83 |
| 392 | Rowe | 2530000 | 1817045.04 | 63.00 | 28841.98 | 2340252.67 | 84.70 | 27629.90 | 411865.00 | 49622.00 | 8.30 | 0.18 | 7.59 |
| 393 | Lowell Middlesex Academy Charter (District) | 4580000 | 2027681.89 | 83.90 | 24167.84 | 2027681.89 | 83.90 | 24167.84 | 743579.00 | 106165.00 | 7.00 | 0.37 | 11.99 |
| 394 | Savoy | 2630000 | 1144630.50 | 45.00 | 25436.23 | 1382283.50 | 64.10 | 21564.49 | 358543.00 | 64026.00 | 5.60 | 0.26 | 8.04 |
395 rows × 13 columns
# Import Class Size and Ethnicity Dataset
ClassSizeEthnicity = pd.read_excel('ClassSizebyRaceEthnicity.xlsx', sheet_name='Class Size by Race Ethnicity Re', skiprows=1, nrows=401)
# Convert to Strings for Regex
ClassSizeEthnicity['District Name'] = ClassSizeEthnicity['District Name'].astype('string')
ClassSizeEthnicity['Total # of Classes'] = ClassSizeEthnicity['Total # of Classes'].astype('string')
ClassSizeEthnicity['Number of Students'] = ClassSizeEthnicity['Number of Students'].astype('string')
# Regex String Replacement/Cleanup, replace does full matching without regex
ClassSizeEthnicity['Total # of Classes'].replace(['\$',' ',','],'',regex=True,inplace=True)
ClassSizeEthnicity['Number of Students'].replace(['\$',' ',','],'',regex=True,inplace=True)
# Conversion to Floats
ClassSizeEthnicity['Total # of Classes'] = ClassSizeEthnicity['Total # of Classes'].astype('float')
ClassSizeEthnicity['Number of Students'] = ClassSizeEthnicity['Number of Students'].astype('float')
# Combine Ethnicity and Class Size Data with AllTowns DataFrame - Name Ethnic Analysis
EthnicAnalysis = ClassSizeEthnicity.merge(AllTowns,on=['District Name','District Code'])
# Generate Charter School Column (includes Charter in Name)
EthnicAnalysis['School Type'] = np.where(EthnicAnalysis['District Name'].str.lower().str.contains('charter'),'Charter', 'Non-Charter')
EthnicAnalysis['School Type'] = EthnicAnalysis['School Type'].astype('category')
EthnicAnalysis.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 395 entries, 0 to 394 Data columns (total 24 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 District Name 395 non-null string 1 District Code 395 non-null int64 2 Total # of Classes 395 non-null float64 3 Average Class Size 395 non-null float64 4 Number of Students 395 non-null float64 5 African American % 395 non-null float64 6 Asian % 395 non-null float64 7 Hispanic % 395 non-null float64 8 White % 395 non-null float64 9 Native American % 395 non-null float64 10 Native Hawaiian, Pacific Islander % 395 non-null float64 11 Multi-Race, Non-Hispanic % 395 non-null float64 12 In-District Expenditures 395 non-null float64 13 Total In-district FTEs 395 non-null float64 14 In-District Expenditures per Pupil 395 non-null float64 15 Total Expenditures 395 non-null float64 16 Total Pupil FTEs 395 non-null float64 17 Total Expenditures per Pupil 395 non-null float64 18 Salary Totals 395 non-null float64 19 Average Salary 395 non-null float64 20 FTE Count 395 non-null float64 21 TeacherProportion 395 non-null float64 22 StudentTeacherRatio 395 non-null float64 23 School Type 395 non-null category dtypes: category(1), float64(21), int64(1), string(1) memory usage: 71.6 KB
# Format Table for 2 decimal places
pd.options.display.float_format = '{:.2f}'.format
AllTowns.describe()
| District Code | In-District Expenditures | Total In-district FTEs | In-District Expenditures per Pupil | Total Expenditures | Total Pupil FTEs | Total Expenditures per Pupil | Salary Totals | Average Salary | FTE Count | TeacherProportion | StudentTeacherRatio | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 395.00 | 395.00 | 395.00 | 395.00 | 395.00 | 395.00 | 395.00 | 395.00 | 395.00 | 395.00 | 395.00 | 395.00 |
| mean | 4652177.22 | 42630924.81 | 2298.60 | 19279.83 | 47803108.11 | 2504.15 | 19506.27 | 16358807.82 | 81386.70 | 189.96 | 0.34 | 11.78 |
| std | 6336749.34 | 82763645.82 | 3647.25 | 4226.71 | 100322337.49 | 4394.63 | 3723.96 | 30366458.45 | 13042.90 | 315.27 | 0.05 | 2.18 |
| min | 10000.00 | 989688.30 | 45.00 | 11417.68 | 1382283.50 | 64.10 | 11417.68 | 322221.00 | 43543.00 | 5.60 | 0.15 | 6.60 |
| 25% | 1595000.00 | 11431131.18 | 569.80 | 16395.80 | 11752696.78 | 597.60 | 16884.49 | 3840204.00 | 73208.00 | 48.80 | 0.31 | 10.53 |
| 50% | 3040000.00 | 25705388.00 | 1372.20 | 18279.46 | 27487113.02 | 1452.20 | 18731.99 | 9246816.00 | 82721.00 | 113.20 | 0.35 | 11.76 |
| 75% | 6015000.00 | 48611728.96 | 2702.05 | 21162.43 | 53580324.12 | 2830.80 | 21266.46 | 19611623.50 | 89558.50 | 229.05 | 0.37 | 12.86 |
| max | 35180000.00 | 1348915645.82 | 48747.80 | 43111.71 | 1676837932.25 | 63064.70 | 35113.70 | 481668763.00 | 117960.00 | 4595.50 | 0.50 | 26.22 |
Most Important
# Generate Quantiles for Evaluation
QuantileData = TownsOnlySpend[['Total Expenditures per Pupil','Total Pupil FTEs']].quantile([0.01,0.05,0.25,0.5,0.75,0.95,0.99])
QuantileData['High Estimate StateWide Revenue (Billions)'] = QuantileData['Total Expenditures per Pupil']*992854/1000000000
QuantileData['High Mass GDP%'] = QuantileData['High Estimate StateWide Revenue (Billions)']/720*100
QuantileData['Low Estimate StateWide Revenue (Billions)'] = QuantileData['Total Expenditures per Pupil']*883000/1000000000
QuantileData['Low Mass GDP%'] = QuantileData['Low Estimate StateWide Revenue (Billions)']/720*100
QuantileData
| Total Expenditures per Pupil | Total Pupil FTEs | High Estimate StateWide Revenue (Billions) | High Mass GDP% | Low Estimate StateWide Revenue (Billions) | Low Mass GDP% | |
|---|---|---|---|---|---|---|
| 0.01 | 13654.55 | 98.03 | 13.56 | 1.88 | 12.06 | 1.67 |
| 0.05 | 15136.86 | 167.55 | 15.03 | 2.09 | 13.37 | 1.86 |
| 0.25 | 16856.38 | 597.40 | 16.74 | 2.32 | 14.88 | 2.07 |
| 0.50 | 18726.81 | 1442.80 | 18.59 | 2.58 | 16.54 | 2.30 |
| 0.75 | 21276.39 | 2815.00 | 21.12 | 2.93 | 18.79 | 2.61 |
| 0.95 | 26312.17 | 7022.00 | 26.12 | 3.63 | 23.23 | 3.23 |
| 0.99 | 32868.30 | 17045.34 | 32.63 | 4.53 | 29.02 | 4.03 |
# Set Chart Styles
sns.set_style('darkgrid')
sns.color_palette('colorblind')
# ScatterPlot
ax = sns.scatterplot(TownsOnlySpend, x='Total Pupil FTEs',y='Total Expenditures per Pupil')
ax.set_title('$15,000 of State Money per Student Preserves Local Variation in School Funding')
ax.axhspan(0,QuantileData.iloc[0,0], alpha=0.2)
ax.text(40000,QuantileData.iloc[0,0],'1st Percentile Spend/Pupil 13654', ha='center', va='bottom')
ax.axhspan(0,QuantileData.iloc[1,0], alpha=0.2)
ax.text(45000,QuantileData.iloc[1,0],'5th Percentile Spend/Pupil 15136', ha='center', va='bottom')
ax.axhline(QuantileData.iloc[2,0])
ax.text(50000,QuantileData.iloc[2,0],'Median Total Spend/Pupil 18726', ha='center', va='bottom')
ax.text(0,0,'Conor Sheehy', ha='left', va='top',alpha=0.2)
Text(0, 0, 'Conor Sheehy')
# 10k pupil zoom in
tenthousandpupilmask = TownsOnlySpend[TownsOnlySpend['Total Pupil FTEs']<10000]
# Zoomed-In Version From Above + Formatting
ax = sns.scatterplot(tenthousandpupilmask, x='Total Pupil FTEs',y='Total Expenditures per Pupil')
ax.set_title('$15,000 of State Money per Student Preserves Local Variation in School Funding (Zoomed in on < 10,000 Pupils)')
ax.axhspan(0,QuantileData.iloc[0,0], alpha=0.2)
ax.text(10500,QuantileData.iloc[0,0],'1st Percentile Spend/Pupil 13,654', ha='left', va='top')
ax.axhspan(0,QuantileData.iloc[1,0], alpha=0.2)
ax.text(10500,QuantileData.iloc[1,0],'5th Percentile Spend/Pupil 15,136', ha='left', va='top')
ax.axhline(QuantileData.iloc[2,0])
ax.text(10500,QuantileData.iloc[2,0],'Median Total Spend/Pupil 18,726', ha='left', va='top')
ax.text(0,0,'Conor Sheehy', ha='left', va='top',alpha=0.2)
Text(0, 0, 'Conor Sheehy')
# Order 2 Regression plot - Potentially Spurious Relationship, especially given low sample of large schools
regorder2 = sns.regplot(TownsOnlySpend, x='Total Pupil FTEs',y='Total Expenditures per Pupil',order=2,ci=None)
regorder2.text(50000,10000,'Conor Sheehy', ha='left', va='top',alpha=0.2)
Text(50000, 10000, 'Conor Sheehy')
Additional Information On Teacher Salaries and School Quality Proxies
# Expenditures and Teacher Resourcing per Student
stuteachperpupil = sns.scatterplot(EthnicAnalysis, x='Total Expenditures per Pupil', y='StudentTeacherRatio', hue = 'School Type', hue_order=['Non-Charter','Charter'])
stuteachperpupil.text(11000,7.5,'Conor Sheehy', ha='left', va='top',alpha=0.2)
Text(11000, 7.5, 'Conor Sheehy')
(so.Plot(EthnicAnalysis, x='Total Expenditures per Pupil', y='StudentTeacherRatio', text='District Name')
.add(so.Dot())
.add(so.Text(valign='top'))
.layout(size=(30,30)))
sns.lmplot(EthnicAnalysis, x='Total Expenditures per Pupil', y='TeacherProportion', hue = 'School Type',hue_order=['Non-Charter','Charter'], ci=False)
<seaborn.axisgrid.FacetGrid at 0x15b809790>
#Significant Variation in Average Teacher Salaries and Total Expenditures Per Pupil
teachersalaryperpupil = sns.scatterplot(EthnicAnalysis,x='Total Expenditures per Pupil',y='Average Salary', hue = 'School Type', hue_order=['Non-Charter','Charter'])
teachersalaryperpupil.text(30000,42000,'Conor Sheehy',alpha=.2)
Text(30000, 42000, 'Conor Sheehy')
sns.lmplot(EthnicAnalysis, x= 'Total Expenditures per Pupil', y= 'Average Salary', hue = 'School Type', hue_order=['Non-Charter','Charter'], ci=False)
<seaborn.axisgrid.FacetGrid at 0x15b914a90>
sns.lmplot(EthnicAnalysis, x= 'Total Expenditures per Pupil', y= 'Average Salary', col='School Type', hue ='School Type', hue_order=['Non-Charter','Charter'], ci=None)
<seaborn.axisgrid.FacetGrid at 0x15b8c5c10>
#Plot Town Names, Expenditures per Pupil and Average Teacher Salary
(so.Plot(EthnicAnalysis, x="Total Expenditures per Pupil", y="Average Salary", text="District Name")
.add(so.Dot())
.add(so.Text(valign="top"))
.layout(size=(30,30)))
#Plot Town Names, Total Pupil FTEs, and Expenditure per Pupil
(so.Plot(AllTowns, x="Total Pupil FTEs", y="Total Expenditures per Pupil", text="District Name")
.add(so.Dot())
.add(so.Text(halign="left"))
.layout(size=(30,10)))
#Please Do Not Draw Causal Relationships from the Scatterplots
#Ignore Relationship between Average Salary and TeacherProportion (of total expenditure) since they are non-independent
ForPairplot = EthnicAnalysis[['Total Pupil FTEs','Total Expenditures per Pupil','Average Salary','FTE Count','School Type','TeacherProportion','StudentTeacherRatio']]
sns.pairplot(ForPairplot, hue = 'School Type', hue_order=['Non-Charter','Charter'], diag_kind='hist')
<seaborn.axisgrid.PairGrid at 0x15c104250>
EthnicPairplot = EthnicAnalysis[['Average Class Size','African American %','Asian %','Hispanic %','White %','Native American %','Native Hawaiian, Pacific Islander %','Multi-Race, Non-Hispanic %','School Type']]
sns.pairplot(EthnicPairplot, hue = 'School Type',hue_order=['Non-Charter','Charter'], diag_kind='hist')
<seaborn.axisgrid.PairGrid at 0x15ba2f690>
Referring to the Above: Are children in Charter Schools more likely to be better resourced than in public schools? The higher the black student percentage of the district the more likely it is to have less teachers per student.
sns.boxplot(data=EthnicAnalysis, x='School Type', y='African American %',hue_order=['Non-Charter','Charter'])
EthnicAnalysis.groupby('School Type')['African American %'].describe()
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| School Type | ||||||||
| Charter | 78.00 | 27.24 | 25.22 | 0.40 | 5.33 | 17.80 | 48.45 | 87.70 |
| Non-Charter | 317.00 | 4.15 | 6.28 | 0.00 | 1.20 | 2.30 | 4.60 | 60.90 |
Mass Schools with more than 10% black students are outliers among public schools.
sns.scatterplot(data=EthnicAnalysis, x='White %', y='Average Class Size', hue = 'School Type', hue_order=['Non-Charter','Charter'], size='Number of Students')
<Axes: xlabel='White %', ylabel='Average Class Size'>
ax1 = sns.scatterplot(data=EthnicAnalysis, x='White %', y='Average Salary', hue = 'School Type', hue_order=['Non-Charter','Charter'], size='Number of Students')
sns.move_legend(ax1, 'upper left', bbox_to_anchor=(1, 1))
ax2 = sns.scatterplot(data=EthnicAnalysis, x='White %', y='Total Expenditures per Pupil', hue = 'School Type', hue_order=['Non-Charter','Charter'], size='Number of Students')
sns.move_legend(ax2, 'upper left', bbox_to_anchor=(1, 1))
EthnicAnalysis['NewtonTrue'] = EthnicAnalysis['District Name'].str.contains('Newton')
Newton = EthnicAnalysis[EthnicAnalysis['District Name'].str.contains('Newton')]
ExcludingNewton = EthnicAnalysis[EthnicAnalysis['NewtonTrue']==False]
ExcludingNewton
| District Name | District Code | Total # of Classes | Average Class Size | Number of Students | African American % | Asian % | Hispanic % | White % | Native American % | ... | Total Expenditures | Total Pupil FTEs | Total Expenditures per Pupil | Salary Totals | Average Salary | FTE Count | TeacherProportion | StudentTeacherRatio | School Type | NewtonTrue | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Abby Kelley Foster Charter Public (District) | 4450000 | 713.00 | 18.90 | 1425.00 | 54.30 | 3.40 | 19.70 | 16.80 | 0.60 | ... | 24004095.80 | 1424.30 | 16853.85 | 7586419.00 | 66422.00 | 114.20 | 0.32 | 12.47 | Charter | False |
| 1 | Abington | 10000 | 1017.00 | 19.80 | 2178.00 | 5.00 | 2.30 | 11.50 | 78.70 | 0.50 | ... | 36223802.52 | 2243.90 | 16143.23 | 12181431.00 | 92005.00 | 132.40 | 0.34 | 16.05 | Non-Charter | False |
| 2 | Academy Of the Pacific Rim Charter Public (Dis... | 4120000 | 201.00 | 19.70 | 538.00 | 62.60 | 0.70 | 28.40 | 6.70 | 0.00 | ... | 11434614.36 | 539.00 | 21216.47 | 3503450.00 | 67505.00 | 51.90 | 0.31 | 10.39 | Charter | False |
| 3 | Acton-Boxborough | 6000000 | 2368.00 | 17.10 | 5246.00 | 3.20 | 33.60 | 6.50 | 51.60 | 0.10 | ... | 101326511.27 | 5344.90 | 18957.61 | 36991103.00 | 91268.00 | 405.30 | 0.37 | 12.88 | Non-Charter | False |
| 4 | Acushnet | 30000 | 400.00 | 15.20 | 913.00 | 1.20 | 1.00 | 4.20 | 89.80 | 0.20 | ... | 18413333.92 | 1235.10 | 14908.37 | 5221793.00 | 76566.00 | 68.20 | 0.28 | 13.53 | Non-Charter | False |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 390 | Winthrop | 3460000 | 661.00 | 20.60 | 1847.00 | 1.40 | 0.70 | 16.00 | 80.50 | 0.10 | ... | 30863993.00 | 1887.20 | 16354.38 | 11547972.00 | 76884.00 | 150.20 | 0.37 | 12.21 | Non-Charter | False |
| 391 | Woburn | 3470000 | 2985.00 | 13.80 | 4332.00 | 7.50 | 7.10 | 12.40 | 68.40 | 0.80 | ... | 92125625.08 | 4494.40 | 20497.87 | 32234181.00 | 89168.00 | 361.50 | 0.35 | 11.90 | Non-Charter | False |
| 392 | Worcester | 3480000 | 11791.00 | 14.10 | 20660.00 | 17.30 | 6.20 | 42.90 | 29.00 | 0.20 | ... | 466801954.33 | 26958.00 | 17315.90 | 171799156.00 | 93339.00 | 1840.60 | 0.37 | 13.05 | Non-Charter | False |
| 393 | Worthington | 3490000 | 60.00 | 6.00 | 58.00 | 0.00 | 0.00 | 5.20 | 91.40 | 1.70 | ... | 1730322.56 | 118.70 | 14577.28 | 322221.00 | 43543.00 | 7.40 | 0.19 | 8.34 | Non-Charter | False |
| 394 | Wrentham | 3500000 | 404.00 | 18.40 | 893.00 | 1.80 | 3.00 | 5.40 | 84.80 | 0.30 | ... | 18253166.12 | 907.80 | 20107.03 | 6481188.00 | 87348.00 | 74.20 | 0.36 | 11.56 | Non-Charter | False |
394 rows × 25 columns
MassExNewtonQuantiles = ExcludingNewton[['Average Class Size',
'In-District Expenditures per Pupil',
'Total Pupil FTEs', 'Total Expenditures per Pupil',
'Average Salary', 'TeacherProportion',
'StudentTeacherRatio','FTE Count','Salary Totals']].quantile([.25,.5,.67,.75,.833,.95,.99])
NewtonComparison = pd.concat([MassExNewtonQuantiles, Newton[['Average Class Size',
'In-District Expenditures per Pupil',
'Total Pupil FTEs', 'Total Expenditures per Pupil',
'Average Salary', 'TeacherProportion',
'StudentTeacherRatio','FTE Count','Salary Totals']]])
NewtonComparison.index = ['25%', 'Median','67%','75%','83.3%','95%','99%','Newton']
NewtonComparison.style.format(precision=2, thousands=",", decimal=".")
NewtonComparison
| Average Class Size | In-District Expenditures per Pupil | Total Pupil FTEs | Total Expenditures per Pupil | Average Salary | TeacherProportion | StudentTeacherRatio | FTE Count | Salary Totals | |
|---|---|---|---|---|---|---|---|---|---|
| 25% | 13.83 | 16393.53 | 597.40 | 16874.23 | 73133.00 | 0.31 | 10.53 | 48.20 | 3838836.00 |
| Median | 15.85 | 18255.37 | 1442.80 | 18726.81 | 82697.00 | 0.35 | 11.77 | 113.10 | 9239410.00 |
| 67% | 17.10 | 20173.37 | 2292.84 | 20374.65 | 87343.17 | 0.36 | 12.52 | 172.71 | 14689377.03 |
| 75% | 17.90 | 21092.40 | 2815.00 | 21239.89 | 89547.50 | 0.37 | 12.87 | 228.22 | 19532510.75 |
| 83.3% | 18.74 | 22718.09 | 3902.69 | 22780.88 | 93060.63 | 0.38 | 13.50 | 297.22 | 26300303.92 |
| 95% | 22.47 | 26630.85 | 6961.17 | 26207.71 | 100951.15 | 0.41 | 15.19 | 514.95 | 46204779.95 |
| 99% | 31.94 | 33661.24 | 17075.39 | 32790.03 | 111053.67 | 0.44 | 17.55 | 1125.32 | 95128871.89 |
| Newton | 17.10 | 22499.29 | 12200.70 | 23431.12 | 93031.00 | 0.36 | 11.02 | 1091.00 | 101497172.00 |
^Comparison between Newton and All other Towns Above
More Newton Information Below:
Newton = EthnicAnalysis[EthnicAnalysis['District Name'].str.contains('Newton')]
Newton
| District Name | District Code | Total # of Classes | Average Class Size | Number of Students | African American % | Asian % | Hispanic % | White % | Native American % | ... | Total Expenditures | Total Pupil FTEs | Total Expenditures per Pupil | Salary Totals | Average Salary | FTE Count | TeacherProportion | StudentTeacherRatio | School Type | NewtonTrue | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 242 | Newton | 2070000 | 5815.00 | 17.10 | 12121.00 | 4.60 | 20.10 | 8.60 | 58.90 | 0.10 | ... | 285876115.00 | 12200.70 | 23431.12 | 101497172.00 | 93031.00 | 1091.00 | 0.36 | 11.02 | Non-Charter | True |
1 rows × 25 columns
FTE refers to Full Time Equivalent, Salary is Average Teacher Salary, Teacher Proportion is the proportion of The District School budget that goes to teacher salaries. Student Teacher Ratio is a calculated ratio of Number of Students to the number of FTE Teachers. 2021 Education Data from DESE (Mass.gov)
sns.histplot(EthnicAnalysis, y='Average Salary', hue = 'School Type', hue_order=['Non-Charter','Charter'], binwidth=2500, binrange=(40000,120000))
<Axes: xlabel='Count', ylabel='Average Salary'>
Proportion of Total District Expenditures going to Teachers (Below)
sns.histplot(EthnicAnalysis, y='TeacherProportion', hue = 'School Type', hue_order=['Non-Charter','Charter'], binwidth=.0125, binrange=(.15,.5))
<Axes: xlabel='Count', ylabel='TeacherProportion'>
sns.scatterplot(EthnicAnalysis, x = 'StudentTeacherRatio', y= 'Average Class Size', size='Number of Students', hue = 'School Type', hue_order=['Non-Charter','Charter'])
<Axes: xlabel='StudentTeacherRatio', ylabel='Average Class Size'>
ax = sns.scatterplot(data=EthnicAnalysis, x='Average Salary', y='Average Class Size', size='Number of Students', hue = 'School Type', hue_order=['Non-Charter','Charter'])
sns.move_legend(ax, "upper left", bbox_to_anchor=(1, 1))